package golf.simulation;

import golf.simulation.dataobject.InvestmentItem;
import golf.simulation.dataobject.SimulationOutputData;
import golf.simulation.dataobject.Transaction;
import golf.simulation.dataobject.TransactionType;
import golf.stockdata.framework.hbmdao.PriceHist;
import golf.stockdata.framework.util.hbm.HibernateUtil;

import java.math.BigDecimal;
import java.sql.Date;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Random;

import org.apache.log4j.Logger;
import org.hibernate.Session;
import org.hibernate.criterion.Property;

public class ForexSimulationFormula1 {

	private static Logger logger = Logger.getLogger(ForexSimulationFormula1.class);
	private static Logger outputLogger = Logger.getLogger("SIMULATION_OUTPUT");
//	private static Logger outputLogger2 = Logger.getLogger("SIMULATION_OUTPUT_2");
	private static Random RANDOM_GENERATOR = new Random();
	
	/**
	 * Big Swing is when the price span across the sell-limit and the cut-limit
	 * in the same day
	 */
	private static int BIG_SWING_COUNTER = 0;
	public static double LOWEST_RUNNING_BALANCE=Double.MAX_VALUE;
	
	public static SimulationOutputData simulate(
				double initialCapital,
				double minimumBalancePercentage, //Guarantee that the whole capital should be kept at this level
				double absoluteMinimumBalance,
				int maxInvestmentItem, // maximum allowed investment items
				double pillowPercentage,
				double sellPercentage,
				double spreadSize, //percentage of spread, normally minus sign (approx -0.35%)
				double gearingRatio, //20(1:20), 25(1:25), 50(1:50)
				int forexLotSize, //e.g. XAUUSD=1, USDEUR=100000
				double numberOfLots,
				int numberOfAllowedLost,
				int inTheLast,
				int randomWaitLimit,
				Date startDate,
				Date endDate,
				String periodType,
				String marketName,
				String currencyPair,
				int smavInput){
	    	
	    	logger.debug(">>> simulate");
	    	
	    	LOWEST_RUNNING_BALANCE=Double.MAX_VALUE;
	    	BIG_SWING_COUNTER = 0;
	    	
	    	if(maxInvestmentItem <= 0){
	    		maxInvestmentItem = 1;
	    	}
	    	
	    	double totalMarginx = initialCapital;
	    	double availableMarginx = totalMarginx;
	    	
	    	ArrayList<InvestmentItem> invstmntStreams = new ArrayList<InvestmentItem>();
	    	
	        Session session = HibernateUtil.createHibernatesession();
	        session.getTransaction().begin();
	        
	        @SuppressWarnings("unchecked")
			ArrayList<PriceHist> priceHistList = (ArrayList<PriceHist>)session.createCriteria(PriceHist.class, "main")
				.add(Property.forName("id.tradingDate").between(startDate, endDate) )
				.add(Property.forName("id.marketName").eq(marketName))
				.add(Property.forName("id.currencyPair").eq(currencyPair))
				.add(Property.forName("id.tradingType").eq(periodType)).list();
	        
	        int i=0;
			int previousMonth = 0;
			double lastMinimumAmount = 0;
			boolean hasDroppedAContractThisMonth = false;
			int contractAfterDropPeriod = 0;
			
	        for(PriceHist priceHist:priceHistList){
	        	i++;
	        	if(i > smavInput){
	        		
	            	double smav = getSMAV(smavInput, priceHist.getId().getTradingDate(), priceHistList);
        			//Money handling and check if we can get a new investment stream
	            	double costOfInvestment = priceHist.getClose().doubleValue() * forexLotSize * numberOfLots / gearingRatio * 2;
	            	
	        		int currentMonth = getMonth(priceHist.getId().getTradingDate());
	        		
	        		if(contractAfterDropPeriod > 0){
	        			contractAfterDropPeriod--;
	        		}
	        		
	        		if(currentMonth != previousMonth
	        				&& contractAfterDropPeriod == 0){
	        			
	        			hasDroppedAContractThisMonth = false;
	        			
	        			logger.debug("Month changed"); 
	        			logger.debug("available margin:" + availableMarginx);
		            	
        				if(availableMarginx < 1000){
        					//For break-point debug
        					System.out.print("");
        				}
            				
	        			if(availableMarginx > 0
	        					&& (availableMarginx - costOfInvestment)/availableMarginx > minimumBalancePercentage
	        					&& invstmntStreams.size() < maxInvestmentItem
	        					&& availableMarginx > absoluteMinimumBalance){
	        				
	        				//Keep lastMinimumAmount for later use
	        				lastMinimumAmount = availableMarginx * minimumBalancePercentage;
	        				//Safe guaranteed remaining, add one more investment stream
	        				invstmntStreams.add(new InvestmentItem());
	        				logger.debug("Added a new contract:" + invstmntStreams.get(invstmntStreams.size() - 1).getUUID());
	        			}
	        		}
	        		
	        		for(InvestmentItem invstmnt : invstmntStreams){
	        			
	        			if(invstmnt.getForceIdle() == 0
	        					&& !invstmnt.isDiscontinued()){
	        				//started investment processing
	        				
	            			logger.debug("Processing:" + invstmnt.getUUID());
	            			
	                    	if(invstmnt.isFirstTime()){
	                    		
	                    		logger.debug("First time block");
	                    		
	                    		invstmnt.setCurrentTransaction(firstTimeProcess(smav, priceHist, pillowPercentage, sellPercentage));
	                    		
	                    		if(invstmnt.getCurrentTransaction() != null){
	    	        				availableMarginx = availableMarginx - costOfInvestment;
	    	        				invstmnt.setMarginCostOfCurrentTransaction(costOfInvestment);
	    	        				outputLogger.info("Cost of adding new contract:" + invstmnt.getUUID() + ";;;;" + priceHist.getId().getTradingDate().toString() + ";;;;;" + (0-costOfInvestment) + ";" + BigDecimal.valueOf(availableMarginx).divide(BigDecimal.ONE, 2, BigDecimal.ROUND_HALF_UP).toString() + ";;" + BigDecimal.valueOf(totalMarginx).divide(BigDecimal.ONE, 2, BigDecimal.ROUND_HALF_UP).toString());
	                    			invstmnt.setFirstTime(false);
	                    		}
	                    	}else{
	                    		
	                    		logger.debug("Not the first time block");
	                    		
	                    		if(invstmnt.getCurrentTransaction() != null){
	                    			
	                        		if(invstmnt.getCurrentTransaction().pillowIsThere()){
	                        			logger.debug("Pillow is still there, check if needs removed");
	                            		pillowSet(priceHist, invstmnt.getCurrentTransaction());
	                        		}
	                        		
	                        		if(!isToHold(priceHist, invstmnt.getCurrentTransaction(), spreadSize, forexLotSize, numberOfLots)){
	                        			
	                        			logger.debug("Not to hold this item, time to take profit or loss");
	                        			
	                	            	costOfInvestment = invstmnt.getMarginCostOfCurrentTransaction();
	                	            	
		    	        				outputLogger.info("add back contract cost:" + invstmnt.getUUID() + ";;;;;;;;;" + BigDecimal.valueOf(costOfInvestment).divide(BigDecimal.ONE, 2, BigDecimal.ROUND_HALF_UP).toString() + ";" + BigDecimal.valueOf(costOfInvestment + availableMarginx).divide(BigDecimal.ONE, 2, BigDecimal.ROUND_HALF_UP).toString() + ";;" + BigDecimal.valueOf(totalMarginx).divide(BigDecimal.ONE, 2, BigDecimal.ROUND_HALF_UP).toString());
	                        			totalMarginx = totalMarginx + invstmnt.getCurrentTransaction().getProfitLoss();
	                        			availableMarginx = availableMarginx + costOfInvestment + invstmnt.getCurrentTransaction().getProfitLoss();
	                        			
	                        			invstmnt.getCurrentTransaction().setSellDate(priceHist.getId().getTradingDate());
	                        			
	                        			outputLogger.info(invstmnt.getUUID() + ";" + invstmnt.getCurrentTransaction().toString() + ";" + BigDecimal.valueOf(availableMarginx).divide(BigDecimal.ONE, 2, BigDecimal.ROUND_HALF_UP).toString() + ";" + BigDecimal.valueOf(lastMinimumAmount).divide(BigDecimal.ONE, 2, BigDecimal.ROUND_HALF_UP).toString() + ";" + BigDecimal.valueOf(totalMarginx).divide(BigDecimal.ONE, 2, BigDecimal.ROUND_HALF_UP).toString());
	                        			
	                        			invstmnt.addTransaction(invstmnt.getCurrentTransaction());
	                        			invstmnt.setCurrentTransaction(null);
	                        			
                        				
                        				logger.debug("Try to continue the investment");
                        				
                        				if(availableMarginx < 1000){
                        					System.out.print("");
                        				}
                        				
                        				double newCostOfInvestment = priceHist.getClose().doubleValue() * forexLotSize * numberOfLots / gearingRatio * 2;
	                        				
										if ((availableMarginx - newCostOfInvestment) / availableMarginx > minimumBalancePercentage
												&& invstmntStreams.size() <= maxInvestmentItem
												&& availableMarginx > absoluteMinimumBalance) {
											
	                            			invstmnt.setCurrentTransaction(firstTimeProcess(smav, priceHist, pillowPercentage, sellPercentage));
	                                		if(invstmnt.getCurrentTransaction() != null){
	                                			invstmnt.setFirstTime(false);
	                	        				availableMarginx = availableMarginx - newCostOfInvestment;
	                	        				invstmnt.setMarginCostOfCurrentTransaction(newCostOfInvestment);
		            	        				outputLogger.info("Cost of adding back contract:" + invstmnt.getUUID() + ";;;;" + priceHist.getId().getTradingDate().toString() + ";;;;;" + (0-newCostOfInvestment) + ";" + BigDecimal.valueOf(availableMarginx).divide(BigDecimal.ONE, 2, BigDecimal.ROUND_HALF_UP).toString());
	                                		}else{
	                                			invstmnt.setFirstTime(true);
	                                		}
	                            			
//	                            			invstmnt.setForceIdle(setForcedIdle(invstmnt.getTransactionList(), numberOfAllowedLost, inTheLast, randomWaitLimit));
	                            			
										}else{
											
											invstmnt.discontinued();
											hasDroppedAContractThisMonth = true;
	                            			
										}
	                        				
	                        		}
	                        		
	                    		}
	                    		
	                    	}
	        			}else{
	                		if(invstmnt.getForceIdle() > 0){
	                			invstmnt.deductForceIdle();
	                		}
	        			}
	        		}
	        		
	            	previousMonth = getMonth(priceHist.getId().getTradingDate());
	            	
	        	}
	        	
	        	if(availableMarginx < LOWEST_RUNNING_BALANCE){
	        		LOWEST_RUNNING_BALANCE = availableMarginx;
	        	}
	        }
	        
	        logger.info("Total Contracts:" + invstmntStreams.size());
	        int countDiscontinue = 0;
	        double fixedPricePerInvestment = 0;
	        for(InvestmentItem item:invstmntStreams){
	        	if(item.isDiscontinued()){
	        		countDiscontinue++;
	        	}else{
	        		if(item.getCurrentTransaction() != null){
		        		fixedPricePerInvestment = fixedPricePerInvestment
		        				+ item.getCurrentTransaction().getBuyPrice() / gearingRatio * numberOfLots * forexLotSize * 2;
	        		}else{
		        		fixedPricePerInvestment = fixedPricePerInvestment
		        				+ item.getLatestTransaction().getBuyPrice() / gearingRatio * numberOfLots * forexLotSize * 2;
	        		}
	        	}
	        }
	        logger.info("Discontinued Contracts:" + countDiscontinue);
	        logger.info("Total active Contracts:" + (invstmntStreams.size() - countDiscontinue) );
	        
	        outputLogger.info("Add back the fixed cost of all holding " + (invstmntStreams.size() - countDiscontinue) + " contracts;;;;;;;;;;" + BigDecimal.valueOf(totalMarginx).divide(BigDecimal.ONE, 2, BigDecimal.ROUND_HALF_UP).toString());
	        
	        
	//        for(Transaction trn:transList){
	//        	System.out.println(trn.toString());
	//        }
	        
	        session.getTransaction().rollback();
	        session.close();
	        
	        SimulationOutputData out = new SimulationOutputData();
	        out.setInvestmentStream(invstmntStreams);
	        out.setRunningBalance(totalMarginx);
	        out.setContractHoldingBalance((invstmntStreams.size() - countDiscontinue) * fixedPricePerInvestment);
	        out.setNumberOfAllActiveContract(invstmntStreams.size() - countDiscontinue);
	        out.setNumberOfAllContract(invstmntStreams.size());
	        out.setNumberOfAllDiscontinuedContract(countDiscontinue);
	        
	        logger.info("BIG Swing = " + BIG_SWING_COUNTER);
	        
	        logger.debug("<<< simulate");
	        return out;
	        
	    }


	private static InvestmentItem findThePoorestPerformingContract(
			ArrayList<InvestmentItem> invstmntStreams, Date currentDate) {
		
		double minProfitMade = Double.MAX_VALUE;
		InvestmentItem theSelected = null;
		
		for(InvestmentItem item:invstmntStreams){
			
			if(!item.isDiscontinued()
					&& item.getTransactionList().size() > 5){
		    	double totalProfitLoss = 0;
	    		for(Transaction txn:item.getTransactionList()){
	    			totalProfitLoss = totalProfitLoss + txn.getProfitLoss();
	    		}
	    		if(totalProfitLoss < minProfitMade
	    				&& minProfitMade > 0){
	    			theSelected = item;
	    		}
			}
		}
		
		return theSelected;
	}


	private static int getMonth(Date dt){
		
		Calendar cal = Calendar.getInstance();
		cal.setTime(dt);
		return cal.get(Calendar.MONTH);
		
	}


	protected static Transaction firstTimeProcess(double smav, PriceHist priceHist, double pillowPercentage, double sellPercentage){
		
		logger.debug(">>> firstTimeProcess");
		Transaction txn = null;
		if(toLong(smav, priceHist)){
			txn = new Transaction(TransactionType.LONG, pillowPercentage, sellPercentage);
		}else if(toShort(smav, priceHist)){
			txn = new Transaction(TransactionType.SHORT, pillowPercentage, sellPercentage);
		}
		
		if(txn != null){
			txn.setBuyDate(priceHist.getId().getTradingDate());
			txn.setBuyPrice(priceHist.getClose().doubleValue());
			logger.debug("<<< firstTimeProcess(new investment transaction)");
			return txn;
		}else{
			logger.debug("<<< firstTimeProcess(null)");
			return null;
		}
		
	}


	private static int setForcedIdle(ArrayList<Transaction> trnList, int numberOfAllowedLost, int inTheLast, int randomWaitLimit){
		
		logger.debug(">>> setForcedIdle");
		
		int forcedIdle = 0;
		int countLoss = 0;
		
		double lastProfitLoss = trnList.get(trnList.size()-1).getProfitLoss();
		
		if(lastProfitLoss < 0){
			logger.debug("last time just loss");
			if(trnList.size() > inTheLast){
				
				for(int i=trnList.size()-1; i>trnList.size()-1-inTheLast; i--){
					if(trnList.get(i).getProfitLoss() < 0){
						countLoss++;
					}
				}
				
				logger.debug("Loss count=" + countLoss + " in the last " + inTheLast + " times");
				
				if(countLoss > numberOfAllowedLost){
					forcedIdle = RANDOM_GENERATOR.nextInt(randomWaitLimit);
				}
				
			}
		}else{
			logger.debug("Not loss recently, no check");
		}
		
		logger.debug("<<< setForcedIdle(" + forcedIdle + ")");
		
		return forcedIdle;
	}


	public static boolean isToHold(
			PriceHist priceHist, 
			Transaction txn, 
			double spreadSize, 
			int forexLotSize,
			double numberOfLots){
		
		logger.debug(">>> isToHold");
		logger.debug("CurrentDate=" + priceHist.getId().getTradingDate().toString());
		logger.debug("TodayHigh=" + priceHist.getHigh());
		logger.debug("TodayLow=" + priceHist.getLow());
		logger.debug("Buy price=" + txn.getBuyPrice());
		logger.debug("Cut Limit=" + txn.getCutLimit());
		logger.debug("Sell Limit=" + txn.getSellLimit());
		logger.debug("Pillow Limit=" + txn.getPillowLimit());
		logger.debug("Pillow exist" + txn.pillowIsThere());
		logger.debug("Transaction type:" + txn.getTransactionType());
		
		
		if(txn.getTransactionType().equals(TransactionType.LONG)){
			if(priceHist.getLow().doubleValue() < txn.getCutLimit()
					&& priceHist.getHigh().doubleValue() > txn.getSellLimit()){
				BIG_SWING_COUNTER++;
			}
		}else{
			if(priceHist.getHigh().doubleValue() > txn.getCutLimit()
					&& priceHist.getLow().doubleValue() < txn.getSellLimit()){
				BIG_SWING_COUNTER++;
			}
		}
		
		if(txn.getTransactionType().equals(TransactionType.LONG)){
			logger.debug("LONG");
			if(priceHist.getLow().compareTo(BigDecimal.valueOf(txn.getCutLimit())) < 0){
				logger.debug("To cut");
				double jumpCutAmount = 0;
				if(txn.getCutLimit() > priceHist.getHigh().doubleValue()){
					logger.debug("Jump low");
					jumpCutAmount = (priceHist.getHigh().doubleValue() - txn.getCutLimit()) * forexLotSize * numberOfLots;
				}
				if(txn.pillowIsThere()){
					txn.setProfitLoss(jumpCutAmount + numberOfLots * spreadSize * txn.getBuyPrice() * 2 * forexLotSize);
				}else{
					double loss = (txn.getCutLimit() - txn.getBuyPrice()) * forexLotSize + (spreadSize * txn.getBuyPrice() * 2);
					txn.setProfitLoss(jumpCutAmount + numberOfLots * loss);
				}
				logger.debug("<<< isToHold(false)");
				return false;
				
			}else if(priceHist.getHigh().compareTo(BigDecimal.valueOf(txn.getSellLimit())) > 0){
				logger.debug("To take profit");
				if(priceHist.getLow().compareTo(BigDecimal.valueOf(txn.getSellLimit())) > 0){
					//The low is higher than the sell limit
					txn.setProfitLoss(numberOfLots * priceHist.getLow().subtract(BigDecimal.valueOf(txn.getPillowLimit())).doubleValue()*forexLotSize + (spreadSize * txn.getBuyPrice() * 2));
				}else{
					txn.setProfitLoss(numberOfLots * ((txn.getSellLimit() - txn.getPillowLimit())*forexLotSize + (spreadSize * txn.getBuyPrice() * 2)));
				}
				logger.debug("<<< isToHold(false)");
				return false;
			}
			
		}else if(txn.getTransactionType().equals(TransactionType.SHORT)){
			if(priceHist.getHigh().compareTo(BigDecimal.valueOf(txn.getCutLimit())) > 0){
				logger.debug("To cut");
				double jumpCutAmount = 0;
				if(txn.getCutLimit() < priceHist.getLow().doubleValue()){
					logger.debug("Jump high");
					jumpCutAmount = (txn.getCutLimit() - priceHist.getLow().doubleValue()) * forexLotSize * numberOfLots;
				}
				if(txn.pillowIsThere()){
					txn.setProfitLoss(jumpCutAmount + numberOfLots * spreadSize * txn.getBuyPrice() * 2 * forexLotSize);
				}else{
					double loss = (txn.getBuyPrice() - txn.getCutLimit()) * forexLotSize + (spreadSize * txn.getBuyPrice() * 2 * forexLotSize);
					txn.setProfitLoss(jumpCutAmount + numberOfLots * loss);
				}
				logger.debug("<<< isToHold(false)");
				return false;
			}else if(priceHist.getLow().compareTo(BigDecimal.valueOf(txn.getSellLimit())) < 0){
				logger.debug("To take profit");
				if(priceHist.getHigh().compareTo(BigDecimal.valueOf(txn.getSellLimit())) < 0){
					//The high is lower than the sell limit
					txn.setProfitLoss(numberOfLots * BigDecimal.valueOf(txn.getPillowLimit()).subtract(priceHist.getHigh()).doubleValue()*forexLotSize + (spreadSize * txn.getBuyPrice() * 2));
				}else{
					txn.setProfitLoss(numberOfLots * ((txn.getPillowLimit() - txn.getSellLimit())*forexLotSize + (spreadSize * txn.getBuyPrice() * 2)));
				}
				return false;
			}
		}
		
		logger.debug("<<< isToHold(true)");
		return true;
	}


	protected static void pillowSet(PriceHist priceHist, Transaction txn){
		
		logger.debug(">>> pillowSet");
		logger.debug("CurrentDate=" + priceHist.getId().getTradingDate().toString());
		logger.debug("TodayHigh=" + priceHist.getHigh());
		logger.debug("TodayLow=" + priceHist.getLow());
		logger.debug("Buy price=" + txn.getBuyPrice());
		logger.debug("Cut Limit=" + txn.getCutLimit());
		logger.debug("Sell Limit=" + txn.getSellLimit());
		logger.debug("Pillow Limit=" + txn.getPillowLimit());
		logger.debug("txn.getTransactionType()=" + txn.getTransactionType());
		
		if(txn.getTransactionType().equals(TransactionType.LONG)){
			if(priceHist.getHigh().compareTo(BigDecimal.valueOf(txn.getPillowLimit())) > 0){
				logger.debug("remove pillow");
				txn.removePillow();
			}
		}else if(txn.getTransactionType().equals(TransactionType.SHORT)){
			if(priceHist.getLow().compareTo(BigDecimal.valueOf(txn.getPillowLimit())) < 0){
				logger.debug("remove pillow");
				txn.removePillow();
			}
		}
		
		logger.debug("<<< pillowSet");
		
	}


	private static double getSMAV(int smav, Date dateOfSMAV, ArrayList<PriceHist> priceHistList){
		
		int i=0;
		for(PriceHist priceHist:priceHistList){
			if(priceHist.getId().getTradingDate().equals(dateOfSMAV)
					|| priceHist.getId().getTradingDate().after(dateOfSMAV)){
				break;
			}
			i++;
		}
		
		if(i < smav -1){
			return -1;
		}
		
		BigDecimal out = BigDecimal.ZERO;
		for(int j=i;j>i-smav;j--){
			out = out.add(priceHistList.get(j).getClose());
		}
		
		out = out.divide(BigDecimal.valueOf(smav), 2, BigDecimal.ROUND_HALF_UP);
		
		
		return out.doubleValue();
	}


	private static boolean toLong(double smav, PriceHist priceHist){
		if(priceHist.getLow().compareTo(BigDecimal.valueOf(smav)) > 0){
			return true;
		}else{
			return false;
		}
	}


	private static boolean toShort(double smav, PriceHist priceHist){
		if(priceHist.getHigh().compareTo(BigDecimal.valueOf(smav)) < 0){
			return true;
		}else{
			return false;
		}
	}
	

}
